BearZPY Blog

Hi, nice to meet you

BearZPY's avatar BearZPY

高性能 log 组件 xlog

高性能 log 组件 xlog

移动开发中遇到的最烦的问题就是用户反馈的问题不能复现,而且也没有日志能够协助定位问题。实际日志系统都会在 release 版本中关闭绝大部分的记录,以免频繁的 IO 读写影响应用的流畅度。在遇到问题之后,想解决问题可能还要联系用户发布特定版本,需要用户配合重现定位问题,实际操作上难度也是很高的。在这种情况下,自己也尝试着优化过日志系统,但是并没有特别好的效果,依然有 CPU 峰值高和丢 log 的现象,最近发现了腾讯开源的微信终端基础组件 Mars,内置了一个高性能 log 组件 xlog,且能够单独提取使用,所以了解学习一下。

Mars 简介

Mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务性无关,平台性无关的基础组件,目前可接入平台:Android、iOS、Mac、Windows、WP 等。

主要包括以下几个部分:

  • comm:可以独立使用的公共库,包括 socket、线程、消息队列、协程等
  • xlog:可以独立使用的高性能日志模块
  • sdt:可以独立使用的网络诊断模块
  • stn:可以独立使用的信令分发网络模块

xlog 方案

xlog 方案简述:

  • 使用流式方式对单行日志进行压缩,压缩加密后写进作为 log 中间 buffer 的 mmap 中。

流式压缩:

  • 流式压缩是相对与多条日志同时压缩的,就是对日志按队列压缩。流式压缩的耗时是使用多条日志同时压缩的 2.5 倍左右,但这个时间基数是微秒级的,而且多条日志同时压缩会造成 CPU 曲线极速升高可能会导致程序卡顿,而流式压缩是把时间分散在整个生命周期内,CPU 的曲线更平滑。压缩使用的是 LZ77 编码。

数据加密:

  • 数据加密是为了安全,数据加密一定要放在压缩后,这样可以让减少数据量,加密使用非对称密钥加密,客户端使用公钥加密,log 文件上传后用私钥解密。加解密需要 PC 机上有 pyelliptic,openssl,python 环境。

mmap 存储:

  • mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。 操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。目前分配内存 150kb。

xlog 方案自定义:

  • xlog 源码都是开源的可以自行修改配制,及是否压缩加密都是可以自定义的。
  • 在架构设计上也考虑了扩展性,比如日志头部的结构体是可以随意修改的。
  • 输出到文件的主要实现是在 Appender 模块也是可插拔的,如果对默认的策略不满意可以自己实现一套。

xlog 其他策略:

  • 每次启动的时候会清理日志,防止占用太多用户磁盘空间。
  • 为了防止 sdcard 被拔掉导致写不了日志,支持设置缓存目录,当 sdcard 插上时会把缓存目录里的日志写入到 sdcard 上.
  • …等

xlog 环境接入

gradle 接入:

gradle 接入使用的日志加密算法是不加密的,如需自定义请参考本地编译。注意 gradle 接入因为考虑依赖包体积的大小,只提供了 armeabi 和 x86_64 两种 CPU 架构的 so, 如果你使用的其他 so 有其他架构的,务必不要使用 gradle 依赖,参考本地编译编出你需要的 so,否则会报 Couldn’t find “xxxx.so”的错误。

dependencies {
    implementation 'com.tencent.mars:mars-xlog:1.0.6'
}

本地编译:

本地编译需要下载 Mars 源码,ndk-r11c 版本,编译脚本都在 libraries 目录,需在该目录下运行,参照 Mars 的 Wiki 页面。

python build_android.py

选择编译下面两个 lib 输出结果全部在 mars_xlog_sdk 目录中
build xlog static libs.
build xlog shared libs.

复制 so 库以及相应的源码到工程中

xlog 使用

添加权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

推荐项目开始时加载 so 库

System.loadLibrary("stlport_shared");
System.loadLibrary("marsxlog");

初始化 xlog

  • Xlog.LEVEL_DEBUG 代表输出 log 等级
  • Xlog.AppednerModeAsync 代表异步记录文件,不建议改成同步
  • cachePath 缓存区,没有外部地址,就存在这里
  • logPath 外部地址
  • nameprefix 日志文件前缀
  • PUB_KEY 加密公钥
final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath();
final String logPath = SDCARD + "/marssample/log";

// this is necessary, or may cash for SIGBUS
final String cachePath = this.getFilesDir() + "/xlog"
final String nameprefix = "MarsSample";
final String PUB_KEY = "";

//init xlog
if (BuildConfig.DEBUG) {
    Xlog.appenderOpen(Xlog.LEVEL_DEBUG, Xlog.AppednerModeAsync, cachePath, logPath, nameprefix, PUB_KEY);
    Xlog.setConsoleLogOpen(true);
} else {
    Xlog.appenderOpen(Xlog.LEVEL_INFO, Xlog.AppednerModeAsync, cachePath, logPath, nameprefix, PUB_KEY);
    Xlog.setConsoleLogOpen(false);
}

Log.setLogImp(new Xlog());

程序结束后反初始化 xlog

  • 这里一定要调用这个,不然在异步记录的时候,最后的部分 log 可能不会写入文件中。
Log.appenderClose();

调用方式:

Log.i(LOG_TAG, "onCreate");

解析 .xlog 文件

  • 使用 decode_mars_nocrypt_log_file.py,可以解析压缩但是未加密的 .xlog 文件,会在当前目录下生产一个 .log 后缀的文件。
  • 使用 decode_mars_crypt_log_file.py,可以解析压缩加密的 .xlog 文件,需要设置 Pubkey 和 Prikey。

额外注意:

  • 如果你的程序使用了多进程,不要把多个进程的日志输出到同一个文件中,保证每个进程独享一个日志文件。
  • 保存 log 的目录请使用单独的目录,不要存放任何其他文件防止被 xlog 自动清理功能误删,会清楚 10 天前的文件。
  • debug 版本下建议把控制台日志打开,日志级别设为 Verbose 或者 Debug, release 版本建议把控制台日志关闭,日志级别使用 Info.
  • cachePath 这个参数必传,而且要 data 下的私有文件目录,例如 /data/data/packagename/files/xlog,mmap 文件会放在这个目录,如果传空串,可能会发生 SIGBUS 的 crash。

补充

实测 Log.appenderClose() 不能在 Application 类的 onTerminate() 函数中调用,根据 API 文档,该函数只在模拟设备时才会调用,真实设备里不会调用该函数。